/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

var zrUtil = require("static/plugins/js/zrender/lib/core/util");

var zrColor = require("static/plugins/js/zrender/lib/tool/color");

var _number = require("../util/number");

var linearMap = _number.linearMap;

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
var each = zrUtil.each;
var isObject = zrUtil.isObject;
var CATEGORY_DEFAULT_VISUAL_INDEX = -1;
/**
 * @param {Object} option
 * @param {string} [option.type] See visualHandlers.
 * @param {string} [option.mappingMethod] 'linear' or 'piecewise' or 'category' or 'fixed'
 * @param {Array.<number>=} [option.dataExtent] [minExtent, maxExtent],
 *                                              required when mappingMethod is 'linear'
 * @param {Array.<Object>=} [option.pieceList] [
 *                                             {value: someValue},
 *                                             {interval: [min1, max1], visual: {...}},
 *                                             {interval: [min2, max2]}
 *                                             ],
 *                                            required when mappingMethod is 'piecewise'.
 *                                            Visual for only each piece can be specified.
 * @param {Array.<string|Object>=} [option.categories] ['cate1', 'cate2']
 *                                            required when mappingMethod is 'category'.
 *                                            If no option.categories, categories is set
 *                                            as [0, 1, 2, ...].
 * @param {boolean} [option.loop=false] Whether loop mapping when mappingMethod is 'category'.
 * @param {(Array|Object|*)} [option.visual]  Visual data.
 *                                            when mappingMethod is 'category',
 *                                            visual data can be array or object
 *                                            (like: {cate1: '#222', none: '#fff'})
 *                                            or primary types (which represents
 *                                            defualt category visual), otherwise visual
 *                                            can be array or primary (which will be
 *                                            normalized to array).
 *
 */

var VisualMapping = function (option) {
    var mappingMethod = option.mappingMethod;
    var visualType = option.type;
    /**
     * @readOnly
     * @type {Object}
     */

    var thisOption = (this.option = zrUtil.clone(option));
    /**
     * @readOnly
     * @type {string}
     */

    this.type = visualType;
    /**
     * @readOnly
     * @type {string}
     */

    this.mappingMethod = mappingMethod;
    /**
     * @private
     * @type {Function}
     */

    this._normalizeData = normalizers[mappingMethod];
    var visualHandler = visualHandlers[visualType];
    /**
     * @public
     * @type {Function}
     */

    this.applyVisual = visualHandler.applyVisual;
    /**
     * @public
     * @type {Function}
     */

    this.getColorMapper = visualHandler.getColorMapper;
    /**
     * @private
     * @type {Function}
     */

    this._doMap = visualHandler._doMap[mappingMethod];

    if (mappingMethod === "piecewise") {
        normalizeVisualRange(thisOption);
        preprocessForPiecewise(thisOption);
    } else if (mappingMethod === "category") {
        thisOption.categories
            ? preprocessForSpecifiedCategory(thisOption) // categories is ordinal when thisOption.categories not specified,
            : // which need no more preprocess except normalize visual.
              normalizeVisualRange(thisOption, true);
    } else {
        // mappingMethod === 'linear' or 'fixed'
        zrUtil.assert(mappingMethod !== "linear" || thisOption.dataExtent);
        normalizeVisualRange(thisOption);
    }
};

VisualMapping.prototype = {
    constructor: VisualMapping,
    mapValueToVisual: function (value) {
        var normalized = this._normalizeData(value);

        return this._doMap(normalized, value);
    },
    getNormalizer: function () {
        return zrUtil.bind(this._normalizeData, this);
    },
};
var visualHandlers = (VisualMapping.visualHandlers = {
    color: {
        applyVisual: makeApplyVisual("color"),

        /**
         * Create a mapper function
         * @return {Function}
         */
        getColorMapper: function () {
            var thisOption = this.option;
            return zrUtil.bind(
                thisOption.mappingMethod === "category"
                    ? function (value, isNormalized) {
                          !isNormalized && (value = this._normalizeData(value));
                          return doMapCategory.call(this, value);
                      }
                    : function (value, isNormalized, out) {
                          // If output rgb array
                          // which will be much faster and useful in pixel manipulation
                          var returnRGBArray = !!out;
                          !isNormalized && (value = this._normalizeData(value));
                          out = zrColor.fastLerp(
                              value,
                              thisOption.parsedVisual,
                              out
                          );
                          return returnRGBArray
                              ? out
                              : zrColor.stringify(out, "rgba");
                      },
                this
            );
        },
        _doMap: {
            linear: function (normalized) {
                return zrColor.stringify(
                    zrColor.fastLerp(normalized, this.option.parsedVisual),
                    "rgba"
                );
            },
            category: doMapCategory,
            piecewise: function (normalized, value) {
                var result = getSpecifiedVisual.call(this, value);

                if (result == null) {
                    result = zrColor.stringify(
                        zrColor.fastLerp(normalized, this.option.parsedVisual),
                        "rgba"
                    );
                }

                return result;
            },
            fixed: doMapFixed,
        },
    },
    colorHue: makePartialColorVisualHandler(function (color, value) {
        return zrColor.modifyHSL(color, value);
    }),
    colorSaturation: makePartialColorVisualHandler(function (color, value) {
        return zrColor.modifyHSL(color, null, value);
    }),
    colorLightness: makePartialColorVisualHandler(function (color, value) {
        return zrColor.modifyHSL(color, null, null, value);
    }),
    colorAlpha: makePartialColorVisualHandler(function (color, value) {
        return zrColor.modifyAlpha(color, value);
    }),
    opacity: {
        applyVisual: makeApplyVisual("opacity"),
        _doMap: makeDoMap([0, 1]),
    },
    liftZ: {
        applyVisual: makeApplyVisual("liftZ"),
        _doMap: {
            linear: doMapFixed,
            category: doMapFixed,
            piecewise: doMapFixed,
            fixed: doMapFixed,
        },
    },
    symbol: {
        applyVisual: function (value, getter, setter) {
            var symbolCfg = this.mapValueToVisual(value);

            if (zrUtil.isString(symbolCfg)) {
                setter("symbol", symbolCfg);
            } else if (isObject(symbolCfg)) {
                for (var name in symbolCfg) {
                    if (symbolCfg.hasOwnProperty(name)) {
                        setter(name, symbolCfg[name]);
                    }
                }
            }
        },
        _doMap: {
            linear: doMapToArray,
            category: doMapCategory,
            piecewise: function (normalized, value) {
                var result = getSpecifiedVisual.call(this, value);

                if (result == null) {
                    result = doMapToArray.call(this, normalized);
                }

                return result;
            },
            fixed: doMapFixed,
        },
    },
    symbolSize: {
        applyVisual: makeApplyVisual("symbolSize"),
        _doMap: makeDoMap([0, 1]),
    },
});

function preprocessForPiecewise(thisOption) {
    var pieceList = thisOption.pieceList;
    thisOption.hasSpecialVisual = false;
    zrUtil.each(pieceList, function (piece, index) {
        piece.originIndex = index; // piece.visual is "result visual value" but not
        // a visual range, so it does not need to be normalized.

        if (piece.visual != null) {
            thisOption.hasSpecialVisual = true;
        }
    });
}

function preprocessForSpecifiedCategory(thisOption) {
    // Hash categories.
    var categories = thisOption.categories;
    var visual = thisOption.visual;
    var categoryMap = (thisOption.categoryMap = {});
    each(categories, function (cate, index) {
        categoryMap[cate] = index;
    }); // Process visual map input.

    if (!zrUtil.isArray(visual)) {
        var visualArr = [];

        if (zrUtil.isObject(visual)) {
            each(visual, function (v, cate) {
                var index = categoryMap[cate];
                visualArr[
                    index != null ? index : CATEGORY_DEFAULT_VISUAL_INDEX
                ] = v;
            });
        } else {
            // Is primary type, represents default visual.
            visualArr[CATEGORY_DEFAULT_VISUAL_INDEX] = visual;
        }

        visual = setVisualToOption(thisOption, visualArr);
    } // Remove categories that has no visual,
    // then we can mapping them to CATEGORY_DEFAULT_VISUAL_INDEX.

    for (var i = categories.length - 1; i >= 0; i--) {
        if (visual[i] == null) {
            delete categoryMap[categories[i]];
            categories.pop();
        }
    }
}

function normalizeVisualRange(thisOption, isCategory) {
    var visual = thisOption.visual;
    var visualArr = [];

    if (zrUtil.isObject(visual)) {
        each(visual, function (v) {
            visualArr.push(v);
        });
    } else if (visual != null) {
        visualArr.push(visual);
    }

    var doNotNeedPair = {
        color: 1,
        symbol: 1,
    };

    if (
        !isCategory &&
        visualArr.length === 1 &&
        !doNotNeedPair.hasOwnProperty(thisOption.type)
    ) {
        // Do not care visualArr.length === 0, which is illegal.
        visualArr[1] = visualArr[0];
    }

    setVisualToOption(thisOption, visualArr);
}

function makePartialColorVisualHandler(applyValue) {
    return {
        applyVisual: function (value, getter, setter) {
            value = this.mapValueToVisual(value); // Must not be array value

            setter("color", applyValue(getter("color"), value));
        },
        _doMap: makeDoMap([0, 1]),
    };
}

function doMapToArray(normalized) {
    var visual = this.option.visual;
    return (
        visual[
            Math.round(
                linearMap(normalized, [0, 1], [0, visual.length - 1], true)
            )
        ] || {}
    );
}

function makeApplyVisual(visualType) {
    return function (value, getter, setter) {
        setter(visualType, this.mapValueToVisual(value));
    };
}

function doMapCategory(normalized) {
    var visual = this.option.visual;
    return visual[
        this.option.loop && normalized !== CATEGORY_DEFAULT_VISUAL_INDEX
            ? normalized % visual.length
            : normalized
    ];
}

function doMapFixed() {
    return this.option.visual[0];
}

function makeDoMap(sourceExtent) {
    return {
        linear: function (normalized) {
            return linearMap(
                normalized,
                sourceExtent,
                this.option.visual,
                true
            );
        },
        category: doMapCategory,
        piecewise: function (normalized, value) {
            var result = getSpecifiedVisual.call(this, value);

            if (result == null) {
                result = linearMap(
                    normalized,
                    sourceExtent,
                    this.option.visual,
                    true
                );
            }

            return result;
        },
        fixed: doMapFixed,
    };
}

function getSpecifiedVisual(value) {
    var thisOption = this.option;
    var pieceList = thisOption.pieceList;

    if (thisOption.hasSpecialVisual) {
        var pieceIndex = VisualMapping.findPieceIndex(value, pieceList);
        var piece = pieceList[pieceIndex];

        if (piece && piece.visual) {
            return piece.visual[this.type];
        }
    }
}

function setVisualToOption(thisOption, visualArr) {
    thisOption.visual = visualArr;

    if (thisOption.type === "color") {
        thisOption.parsedVisual = zrUtil.map(visualArr, function (item) {
            return zrColor.parse(item);
        });
    }

    return visualArr;
}
/**
 * Normalizers by mapping methods.
 */

var normalizers = {
    linear: function (value) {
        return linearMap(value, this.option.dataExtent, [0, 1], true);
    },
    piecewise: function (value) {
        var pieceList = this.option.pieceList;
        var pieceIndex = VisualMapping.findPieceIndex(value, pieceList, true);

        if (pieceIndex != null) {
            return linearMap(
                pieceIndex,
                [0, pieceList.length - 1],
                [0, 1],
                true
            );
        }
    },
    category: function (value) {
        var index = this.option.categories
            ? this.option.categoryMap[value]
            : value; // ordinal

        return index == null ? CATEGORY_DEFAULT_VISUAL_INDEX : index;
    },
    fixed: zrUtil.noop,
};
/**
 * List available visual types.
 *
 * @public
 * @return {Array.<string>}
 */

VisualMapping.listVisualTypes = function () {
    var visualTypes = [];
    zrUtil.each(visualHandlers, function (handler, key) {
        visualTypes.push(key);
    });
    return visualTypes;
};
/**
 * @public
 */

VisualMapping.addVisualHandler = function (name, handler) {
    visualHandlers[name] = handler;
};
/**
 * @public
 */

VisualMapping.isValidType = function (visualType) {
    return visualHandlers.hasOwnProperty(visualType);
};
/**
 * Convinent method.
 * Visual can be Object or Array or primary type.
 *
 * @public
 */

VisualMapping.eachVisual = function (visual, callback, context) {
    if (zrUtil.isObject(visual)) {
        zrUtil.each(visual, callback, context);
    } else {
        callback.call(context, visual);
    }
};

VisualMapping.mapVisual = function (visual, callback, context) {
    var isPrimary;
    var newVisual = zrUtil.isArray(visual)
        ? []
        : zrUtil.isObject(visual)
        ? {}
        : ((isPrimary = true), null);
    VisualMapping.eachVisual(visual, function (v, key) {
        var newVal = callback.call(context, v, key);
        isPrimary ? (newVisual = newVal) : (newVisual[key] = newVal);
    });
    return newVisual;
};
/**
 * @public
 * @param {Object} obj
 * @return {Object} new object containers visual values.
 *                 If no visuals, return null.
 */

VisualMapping.retrieveVisuals = function (obj) {
    var ret = {};
    var hasVisual;
    obj &&
        each(visualHandlers, function (h, visualType) {
            if (obj.hasOwnProperty(visualType)) {
                ret[visualType] = obj[visualType];
                hasVisual = true;
            }
        });
    return hasVisual ? ret : null;
};
/**
 * Give order to visual types, considering colorSaturation, colorAlpha depends on color.
 *
 * @public
 * @param {(Object|Array)} visualTypes If Object, like: {color: ..., colorSaturation: ...}
 *                                     IF Array, like: ['color', 'symbol', 'colorSaturation']
 * @return {Array.<string>} Sorted visual types.
 */

VisualMapping.prepareVisualTypes = function (visualTypes) {
    if (isObject(visualTypes)) {
        var types = [];
        each(visualTypes, function (item, type) {
            types.push(type);
        });
        visualTypes = types;
    } else if (zrUtil.isArray(visualTypes)) {
        visualTypes = visualTypes.slice();
    } else {
        return [];
    }

    visualTypes.sort(function (type1, type2) {
        // color should be front of colorSaturation, colorAlpha, ...
        // symbol and symbolSize do not matter.
        return type2 === "color" &&
            type1 !== "color" &&
            type1.indexOf("color") === 0
            ? 1
            : -1;
    });
    return visualTypes;
};
/**
 * 'color', 'colorSaturation', 'colorAlpha', ... are depends on 'color'.
 * Other visuals are only depends on themself.
 *
 * @public
 * @param {string} visualType1
 * @param {string} visualType2
 * @return {boolean}
 */

VisualMapping.dependsOn = function (visualType1, visualType2) {
    return visualType2 === "color"
        ? !!(visualType1 && visualType1.indexOf(visualType2) === 0)
        : visualType1 === visualType2;
};
/**
 * @param {number} value
 * @param {Array.<Object>} pieceList [{value: ..., interval: [min, max]}, ...]
 *                         Always from small to big.
 * @param {boolean} [findClosestWhenOutside=false]
 * @return {number} index
 */

VisualMapping.findPieceIndex = function (
    value,
    pieceList,
    findClosestWhenOutside
) {
    var possibleI;
    var abs = Infinity; // value has the higher priority.

    for (var i = 0, len = pieceList.length; i < len; i++) {
        var pieceValue = pieceList[i].value;

        if (pieceValue != null) {
            if (
                pieceValue === value || // FIXME
                // It is supposed to compare value according to value type of dimension,
                // but currently value type can exactly be string or number.
                // Compromise for numeric-like string (like '12'), especially
                // in the case that visualMap.categories is ['22', '33'].
                (typeof pieceValue === "string" && pieceValue === value + "")
            ) {
                return i;
            }

            findClosestWhenOutside && updatePossible(pieceValue, i);
        }
    }

    for (var i = 0, len = pieceList.length; i < len; i++) {
        var piece = pieceList[i];
        var interval = piece.interval;
        var close = piece.close;

        if (interval) {
            if (interval[0] === -Infinity) {
                if (littleThan(close[1], value, interval[1])) {
                    return i;
                }
            } else if (interval[1] === Infinity) {
                if (littleThan(close[0], interval[0], value)) {
                    return i;
                }
            } else if (
                littleThan(close[0], interval[0], value) &&
                littleThan(close[1], value, interval[1])
            ) {
                return i;
            }

            findClosestWhenOutside && updatePossible(interval[0], i);
            findClosestWhenOutside && updatePossible(interval[1], i);
        }
    }

    if (findClosestWhenOutside) {
        return value === Infinity
            ? pieceList.length - 1
            : value === -Infinity
            ? 0
            : possibleI;
    }

    function updatePossible(val, index) {
        var newAbs = Math.abs(val - value);

        if (newAbs < abs) {
            abs = newAbs;
            possibleI = index;
        }
    }
};

function littleThan(close, a, b) {
    return close ? a <= b : a < b;
}

var _default = VisualMapping;
module.exports = _default;
